--[[

    Player Jetpack plugin.
    Version 01/03/2019.

    Description:
    A Lua plugin to brings a new feature to the players, a functional jetpack.


        MIT License

        Copyright (c) 2018 Anggara Yama Putra

        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.

]]


-- Remove unecessary table(s).
Jetpack.UI = nil;


-- Local variables.
local debug = Jetpack.Debug;
local module = Jetpack.Game;
local playerModule = Jetpack.Game.Player;
local vectorModule = Jetpack.Vector;
local syncvalue = Jetpack.SyncValue;
local enum = Jetpack.Enum;
local plrBtnPressingBit = {};
local plrMoveDirBit = {};
local plrLastVelZ = {}
local plrLastPos = {}
local plrNoSpace = {};
local plrNoToggle = {};
for id = 1, 32
do
    plrBtnPressingBit[id]   = 0;
    plrMoveDirBit[id]       = 0;
    plrLastVelZ[id]         = 0;
    plrNoSpace[id]          = false;
    plrNoToggle[id]         = false;
    plrLastPos[id]          = {x=0,y=0,z=0};
end


-- SyncValue init(s).
syncvalue.HasJetpack.Instances = {};
syncvalue.OnOff.Instances = {};
syncvalue.Fuel.Instances = {};


-- Creates all sync variables.
for i = 1, 32
do
    local varName = string.format( syncvalue.HasJetpack.NAME , i );
    syncvalue.HasJetpack.Instances[ i ] = Game.SyncValue.Create( varName );
    if syncvalue.HasJetpack.Instances[ i ] ~= nil
    then
        syncvalue.HasJetpack.Instances[ i ].value = module.DEFAULT_GIVE;
        debug:print( string.format("HasJetpack:Create() (%s)", varName ) );
    end


    varName = string.format( syncvalue.OnOff.NAME , i );
    syncvalue.OnOff.Instances[ i ] = Game.SyncValue.Create( varName );
    if syncvalue.OnOff.Instances[ i ] ~= nil
    then
        syncvalue.OnOff.Instances[ i ].value = module.DEFAULT_ON;
        debug:print( string.format("OnOff:Create() (%s)", varName ) );
    end


    varName = string.format( syncvalue.Fuel.NAME , i );
    syncvalue.Fuel.Instances[ i ] = Game.SyncValue.Create( varName );
    if syncvalue.Fuel.Instances[ i ] ~= nil
    then
        syncvalue.Fuel.Instances[ i ].value = module.FUEL_INIT;
        debug:print( string.format("Fuel:Create() (%s)", varName ) );
    end
end


-- Gets the player's HasJetpack sync variable.
function syncvalue.HasJetpack:Get( index )
    return self.Instances[ index ];
end


-- Gets the player's OnOff sync variable.
function syncvalue.OnOff:Get( index )
    return self.Instances[ index ];
end


-- Gets the player's Fuel sync variable.
function syncvalue.Fuel:Get( index )
    return self.Instances[ index ];
end


-- Checks whether player has jetpack.
function playerModule:HasJetpack( player )
    if player == nil then return false; end

    local var = syncvalue.HasJetpack:Get( player.index );
    if var == nil then return false; end

    return var.value;
end


-- Checks whether player jetpack is on.
function playerModule:IsOn( player )
    if player == nil then return false; end
    if not self:HasJetpack( player ) then return false; end

    local var = syncvalue.OnOff:Get( player.index );
    if var == nil then return false; end

    return var.value;
end


-- Gets the player jetpack fuel value.
function playerModule:GetFuel( player )
    if player == nil then return -1; end
    if not self:HasJetpack( player ) then return -1; end

    local var = syncvalue.Fuel:Get( player.index );
    if var == nil then return -1; end

    return var.value;
end


-- Checks whether the player jetpack fuel is empty.
function playerModule:IsFuelEmpty( player )
    return self:GetFuel( player ) <= 0;
end


-- Sets the player jetpack fuel with given value.
function playerModule:SetFuel( player, value )
    if player == nil
    then
        debug:print( "JP.Plr:SetFuel() player is nil." );
        return;
    end

    if not self:HasJetpack( player ) 
    then 
        debug:print( "JP.Plr:SetFuel() doesn't has jetpack." );
        return; 
    end

    local var = syncvalue.Fuel:Get( player.index );
    if var == nil
    then 
        debug:print( "JP.Plr:SetFuel() var is nil." );
        return; 
    end

    if value > module.FUEL_MAX
    then
        debug:print( "JP.Plr:SetFuel() Set to max." );
        value = module.FUEL_MAX;
    end

    var.value = value;
    debug:print("JP.Plr:SetFuel() Success.");
end


-- Gives the player a jetpack.
function playerModule:Give( player )
    if player == nil
    then
        debug:print( "JP.Plr:Give() player is nil." );
        return;
    end

    if self:HasJetpack( player ) 
    then 
        debug:print( "JP.Plr:Give() already has jetpack." );
        return; 
    end

    local var = syncvalue.HasJetpack:Get( player.index );
    if var == nil 
    then 
        debug:print( "JP.Plr:Give() var is nil." );
        return; 
    end

    var.value = true;
    self:SetFuel( player, module.FUEL_INIT );
    debug:print("JP.Plr:Give() Success.");
end


-- Removes the jetpack from player.
function playerModule:Remove( player )
    if player == nil
    then
        debug:print( "JP.Plr:Remove() player is nil." );
        return;
    end

    if not self:HasJetpack( player ) 
    then 
        debug:print( "JP.Plr:Remove() doesn't has jetpack." );
        return; 
    end

    local var = syncvalue.HasJetpack:Get( player.index );
    if var == nil 
    then 
        debug:print( "JP.Plr:Remove() var is nil." );
        return; 
    end

    var.value = false;
    debug:print("JP.Plr:Remove() Success.");
end


-- Sets player jetpack on/off toggle.
function playerModule:SetToggle( player , value )
    if player == nil
    then
        debug:print( "JP.Plr:SetToggle() player is nil." );
        return;
    end

    if not self:HasJetpack( player ) 
    then 
        debug:print( "JP.Plr:SetToggle() doesn't has jetpack." );
        return; 
    end

    local var = syncvalue.OnOff:Get( player.index );
    if var == nil 
    then 
        debug:print( "JP.Plr:SetToggle() var is nil." );
        return; 
    end

    var.value = value;
    debug:print("JP.Plr:SetToggle() Success.");
end


-- OnUpdate() hook
function module:OnUpdate( time )
    for index = 1, 32
    do
        local player = Game.Player.Create( index );
        if player ~= nil
        then
            local btnPress = plrBtnPressingBit[index];
            local moveDir = plrMoveDirBit[index];

            -- Do button checks.
            if btnPress & enum.BUTTON.PRESSING.TOGGLE ~= 0
            then
                if not plrNoToggle[index]
                then
                    playerModule:SetToggle( player , not playerModule:IsOn(player) );
                end
            end
            plrNoToggle[index] = (btnPress & enum.BUTTON.PRESSING.TOGGLE ~= 0);

            if btnPress & enum.BUTTON.PRESSING.UPWARD ~= 0
            then
                -- fallen bugfix.
                -- need to release space before continue.
                if not plrNoSpace[index]
                then
                    moveDir = moveDir | enum.DIRECTION.UPWARD;
                end
            else
                plrNoSpace[index] = false;
                moveDir = moveDir & ~(enum.DIRECTION.UPWARD);
            end

            if btnPress & enum.BUTTON.PRESSING.FORWARD ~= 0
            then
                moveDir = moveDir | enum.DIRECTION.FORWARD;
            else
                moveDir = moveDir & ~(enum.DIRECTION.FORWARD);
            end

            if btnPress & enum.BUTTON.PRESSING.BACKWARD ~= 0
            then
                moveDir = moveDir | enum.DIRECTION.BACKWARD;
            else
                moveDir = moveDir & ~(enum.DIRECTION.BACKWARD);
            end

            if btnPress & enum.BUTTON.PRESSING.LEFT ~= 0
            then
                moveDir = moveDir | enum.DIRECTION.LEFT;
            else
                moveDir = moveDir & ~(enum.DIRECTION.LEFT);
            end

            if btnPress & enum.BUTTON.PRESSING.RIGHT ~= 0
            then
                moveDir = moveDir | enum.DIRECTION.RIGHT;
            else
                moveDir = moveDir & ~(enum.DIRECTION.RIGHT);
            end

            -- Do movement direction checks.
            local doRegen = true;
            local doStrafe = false;
            local velocity = player.velocity;
            local position = player.position;
            local angles   = vectorModule:VectorAngles( velocity );
            
            -- Player is alive.
            if player.health > 0
            then
                -- Fuel is not empty.
                if not playerModule:IsFuelEmpty( player )
                then
                    -- Jetpack is on.
                    if playerModule:IsOn( player )
                    then
                        local speed     = velocity.z;
                        local maxspeed;
                        local power;

                        -- Flying.
                        if btnPress & enum.BUTTON.PRESSING.LANDING == 0
                        then
                            -- Moves up.
                            if moveDir & enum.DIRECTION.UPWARD ~= 0
                            then
                                power = self.MOVE_UPWARD_SPEED_RATE + self.MOVE_UPWARD_SPEED_EXTRA;
                                if not (speed == 0 and plrLastVelZ[index] == power)
                                then
                                    maxspeed  = self.MOVE_UPWARD_SPEED_MAX + self.MOVE_UPWARD_SPEED_EXTRA;
                                    if speed < maxspeed
                                    then
                                        doRegen = false;

                                        if speed < 0
                                        then
                                            -- Jetpacks get more power on the way down, it helps landing.
                                            power = power * 1.15;
                                        end

                                        speed = speed + power;
                                        if speed > maxspeed
                                        then
                                            speed = maxspeed;
                                        end
                                    end
                                    velocity.z = speed;
                                else
                                    plrNoSpace[index] = true;
                                end
                            end

                            -- Allows fly movements only when 'on air'.
                            if speed ~= 0
                            then
                                power = self.MOVE_SPEED_RATE;
                                local range = vectorModule:Range90Angle( angles.y );
                                local quadrant = vectorModule:GetQuadrant( angles.y );
                                
                                -- Moves forward.
                                if moveDir & enum.DIRECTION.FORWARD ~= 0
                                then
                                    doRegen = false;
                                    doStrafe = true;


                                    local power_x, power_y = (1-range), range;
                                    if quadrant == 1 or quadrant == 3
                                    then
                                        power_x, power_y = power_y, power_x;
                                    end

                                    power_x = power_x * power;
                                    power_y = power_y * power;

                                    if quadrant == 2 or quadrant == 3 then
                                        power_x = power_x * -1;
                                    end
                                    if quadrant == 3 or quadrant == 4 then
                                        power_y = power_y * -1;
                                    end

                                    --[[
                                    print( power_x );
                                    print( power_y .. " ++" );
                                    --]]

                                    velocity.x = velocity.x + power_x;
                                    velocity.y = velocity.y + power_y;
                                end

                                --[[
                                -- Moves backward.
                                if moveDir & enum.DIRECTION.BACKWARD ~= 0
                                then
                                    doRegen = false;
                                    doStrafe = true;
                                end

                                -- Moves left.
                                if moveDir & enum.DIRECTION.LEFT ~= 0
                                then
                                    doRegen = false;
                                    doStrafe = true;
                                end

                                -- Moves right.
                                if moveDir & enum.DIRECTION.RIGHT ~= 0
                                then
                                    doRegen = false;
                                    doStrafe = true;
                                end
                                --]]
                            end
                        -- Landing.
                        else
                            -- Z-axis.
                            speed    = velocity.z;
                            maxspeed = self.LANDING_SPEED_STABLE;
                            if speed ~= 0 and speed ~= -(maxspeed)
                            then
                                doRegen = false;
                                power = math.abs(speed/maxspeed);
                                if power > 1 then power = 1; end

                                power = power * (self.MOVE_UPWARD_SPEED_RATE + self.MOVE_UPWARD_SPEED_EXTRA);
                                if speed > 0
                                then
                                    power = -power;
                                end

                                velocity.z = speed + power;
                            end

                            -- Slow downs strafe movements only when 'on air'.
                            if speed ~= 0
                            then
                                -- Y-axis.
                                speed    = velocity.y;
                                if speed ~= 0 
                                then
                                    doRegen = false;
                                    power = math.abs(speed);
                                    if power > self.MOVE_SPEED_RATE then power = self.MOVE_SPEED_RATE; end
                                    if speed > 0
                                    then
                                        power = -power;
                                    end

                                    velocity.y = speed + power;
                                end

                                -- X-axis.
                                speed    = velocity.x;
                                if speed ~= 0
                                then
                                    doRegen = false;
                                    power = math.abs(speed);
                                    if power > self.MOVE_SPEED_RATE then power = self.MOVE_SPEED_RATE; end
                                    if speed > 0
                                    then
                                        power = -power;
                                    end

                                    velocity.x = speed + power;
                                end
                            end
                        end
                        
                        -- CheckAndScale.
                        speed = vectorModule:Length2D( velocity );
                        if doStrafe
                        then
                            maxspeed = self.MOVE_SPEED_MAX;
                            if speed > maxspeed
                            then
                                local Ratio = maxspeed/speed;
                                velocity.x = velocity.x * Ratio;
                                velocity.y = velocity.y * Ratio;
                            end
                        end

                        -- Updates player velocity.
                        player.velocity = velocity;
                    end
                -- Fuel is empty and is flying.
                elseif velocity.z ~= 0
                then
                    -- Do not regenerate fuel on air.
                    doRegen = false;
                end
            end

            --[[
            print( velocity.x );
            print( velocity.y );
            print( velocity.z );
            print( angles.y .. " --" );
            print( "\n\n" );
            --]]
            
            plrLastVelZ[index] = player.velocity.z;
            plrLastPos[index] = position;

            -- Updates fuel.
            local fuelVar = syncvalue.Fuel:Get( index );
            if fuelVar ~= nil
            then
                local value = fuelVar.value;
                if doRegen
                then
                    if value < Jetpack.FUEL_MAX
                    then
                        value = value + self.FUEL_REGEN_RATE;
                        if value > Jetpack.FUEL_MAX
                        then
                            value = Jetpack.FUEL_MAX;
                        end
                    end
                else
                    if value > 0
                    then
                        value = value - self.FUEL_DEPLETION_RATE;
                        if value < 0
                        then
                            value = 0;
                        end
                    end
                end

                -- Updates it when needed. (to avoid debug spam lel.)
                if fuelVar.value ~= value
                then
                    fuelVar.value = value;
                end

                -- Clears button pressing bitflags.
                plrBtnPressingBit[ index ] = 0;
            end
        end
    end
end


-- OnPlayerSignal() hook.
function module:OnPlayerSignal( player, signal )
    local index = player.index;
    local btnPress = plrBtnPressingBit[index];
    if signal == enum.SIGNAL.TOGGLE
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.PRESSING.TOGGLE;
    end

    if signal == enum.SIGNAL.LANDING
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.PRESSING.LANDING;
    end

    if signal == enum.SIGNAL.MOVE_FORWARD
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.PRESSING.FORWARD;
    end

    if signal == enum.SIGNAL.MOVE_BACKWARD
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.PRESSING.BACKWARD;
    end

    if signal == enum.SIGNAL.MOVE_LEFT
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.PRESSING.LEFT;
    end

    if signal == enum.SIGNAL.MOVE_RIGHT
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.PRESSING.RIGHT;
    end

    if signal == enum.SIGNAL.MOVE_UPWARD
    then
        plrBtnPressingBit[index] = btnPress | enum.BUTTON.PRESSING.UPWARD;
    end
end


-- Default hook executions.
function Game.Rule:OnUpdate( time )
    module:OnUpdate( time )
end
function Game.Rule:OnPlayerSignal( player, signal )
    module:OnPlayerSignal( player, signal );
end


print( "Game.Jetpack.Main is loaded!" );

